/*
  AdViEmulator - AdventureVision emulator
  Copyright (C) 2012-2013  JustBurn

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <QApplication>
#include <QDesktopWidget>
#include <QSettings>
#include <QtGui>
#include <QFile>

#include "mainwindow.h"
#include "cpuwindow.h"
#include "apuwindow.h"
#include "memorywindow.h"
#include "hardwarewindow.h"
#include "tracewindow.h"
#include "optionswindow.h"
#include "aboutwindow.h"

#include "wipwarningwindow.h"

#define DEF_WIDTH	384
#define DEF_HEIGHT	300

MainWindow::MainWindow(QWidget *parent)
	: QMainWindow(parent)
	, emuTickTimer(new QTimer(this))
	, emuElapseTimer(new QElapsedTimer())
	, ui(new Ui::MainWindow)
{
	ui->setupUi(this);
	statusBarIcon.setPixmap(QPixmap(":/images/stopped"));
	statusBarIcon.setScaledContents(false);
	statusBarIcon.setAlignment(Qt::AlignCenter);
	statusBarIcon.setContentsMargins(4, 0, 8, 0);
	ui->statusbar->insertPermanentWidget(0, &statusBarIcon, 0);
	setAcceptDrops(true);
	ui->glwidget->setAcceptDrops(true);

	// Pre-calculate formulas
	CPUCycles2MS = 733333.0 / 1000.0;
	emuTotalCycles = 0;

	// Init cores
	avemu.Reset();
	avemu.SetState(false);
	ui->glwidget->attachEmu(&avemu);

	// Load firmwares
	OnMenuReloadBIOS();
	openROM = "";

	// Setup timers
	connect(emuTickTimer, SIGNAL(timeout()), SLOT(EmuTick()));
	emuTickTimer->start(20);
	emuElapseTimer->start();
	emuLastEmuTick = emuElapseTimer->elapsed();
	emuLastFPSTick = emuLastEmuTick;
	QTimer::singleShot(0, this, SLOT(EmuStarted()));
}

MainWindow::~MainWindow()
{
	emuTickTimer->stop();
	delete ui;
}

void MainWindow::ProcessArgs(int argc, char **argv)
{
	Settings::fileName = QFileInfo(argv[0]).absolutePath() + "/AdViEmulator.ini";
	if (argc >= 2) openROM = argv[1];
}

bool MainWindow::OpenROM(QString filename)
{
	QFile file(filename);
	QString errormsg;
	qint64 numread;
	openROM = filename;
	if (!file.open(QIODevice::ReadOnly)) {
		errormsg = "Can't open file " + filename;
		goto error;
	}
	numread = file.read((char *)avemu.ROM, 4096);
	if (numread == -1) {
		errormsg = "Failed to read file";
		goto error;
	}
	file.close();
	return true;
error:
	file.close();
	QMessageBox msgBox(QMessageBox::Critical, "Error", errormsg);
	msgBox.exec();
	return false;
}

void MainWindow::LoadSettings(void)
{
	// Load global settings
	Settings::LoadSettings();

	// Update settings changes
	ui->glwidget->colRed = Settings::o.colorRed / 255.0f;
	ui->glwidget->colGreen = Settings::o.colorGreen / 255.0f;
	ui->glwidget->colBlue = Settings::o.colorBlue / 255.0f;
	ui->glwidget->pixBar = Settings::o.pixelBar;
	avemu.SetVideoMode(Settings::o.columnMode, Settings::o.analogDisplay, Settings::o.analogDecay);
	avemu.SetVolume(Settings::o.volume);
	avemu.SetAPUFreq(Settings::o.apuFreq);
	sndOut.SetBufferSize(Settings::o.soundSamples);
	sndOut.Enable(Settings::o.soundEnabled);
}

void MainWindow::SaveSettings(void)
{
	// Save global settings
	Settings::SaveSettings();
}

void MainWindow::SetState(bool enable)
{
	avemu.SetState(enable);
	if (enable) {
		statusBarIcon.setPixmap(QPixmap(":/images/running"));
		sndOut.Play(Settings::o.soundEnabled);
	} else {
		setWindowTitle("AdViEmulator");
		statusBarIcon.setPixmap(QPixmap(":/images/stopped"));
		sndOut.Play(false);
	}
}

void MainWindow::TempState(bool temp)
{
	if (temp) {
		avemu.SuspendState();
		setWindowTitle("AdViEmulator");
		statusBarIcon.setPixmap(QPixmap(":/images/halted"));
		sndOut.Play(false);
	} else {
		avemu.ResumeState();
		if (avemu.IsRunning()) {
			statusBarIcon.setPixmap(QPixmap(":/images/running"));
			sndOut.Play(Settings::o.soundEnabled);
		} else {
			statusBarIcon.setPixmap(QPixmap(":/images/stopped"));
		}
	}
}

void MainWindow::EmuStarted(void)
{
	// Resize to default
	setWindowState(windowState() & ~(Qt::WindowMinimized | Qt::WindowMaximized));
	this->resize(DEF_WIDTH, DEF_HEIGHT + this->statusBar()->height() + this->menuBar()->height());
	hide();

	// Load settings
	Settings::o.winX = pos().x();
	Settings::o.winY = pos().y();
	Settings::o.winWidth = size().width();
	Settings::o.winHeight = size().height();
	LoadSettings();
	resize(Settings::o.winWidth, Settings::o.winHeight);
	move(Settings::o.winX, Settings::o.winY);
	rebuildRecentList();

	// Show warning box
	if (Settings::o.WIPwarning) {
		WIPWarningWindow wipwnd;
		wipwnd.resize(wipwnd.sizeHint());
		wipwnd.exec();
		SaveSettings();
	}
	show();

	// Autorun a ROM
	if (!openROM.isEmpty()) {
		if (OpenROM(openROM)) {
			addRecentList(openROM);
			avemu.Reset();
			emit emuStateChanged(4);
			if (Settings::o.autostart) SetState(true);
		}
	}
}

void MainWindow::closeEvent(QCloseEvent *event)
{
	// Save settings
	SaveSettings();

	// Accept to close
	event->accept();
}

void MainWindow::moveEvent(QMoveEvent *event)
{
	if (!this->isMaximized() && !this->isMinimized()) {
		Settings::o.winX = event->pos().x();
		Settings::o.winY = event->pos().y();
	}
}

void MainWindow::resizeEvent(QResizeEvent *event)
{
	if (!this->isMaximized() && !this->isMinimized()) {
		Settings::o.winWidth = event->size().width();
		Settings::o.winHeight = event->size().height();
	}
}

void MainWindow::EmuTick(void)
{
	// Refresh GL
	ui->glwidget->update();

	// Exit if emulator not running
	if (!avemu.IsRunning()) return;
	quint64 elapsetm = emuElapseTimer->elapsed();

	// Emulate!
	if (sndOut.enabled) {
		int buffs = sndOut.GetBuffersFree();
		while (buffs--) {
			emuTotalCycles += avemu.Emulate(true, 256, sndOut.WBufferSize);
			avemu.GetSamples(sndOut.WBuffer, sndOut.WBufferSize);
			sndOut.BufferSend();
			if (avemu.breakExecution) break;
		}
		emuLastEmuTick = elapsetm;
	} else {
		if (emuTotalCycles < 733333) {
			quint32 emuCycles = (quint32)(CPUCycles2MS * (elapsetm - emuLastEmuTick));
			if (emuCycles >= 733333) emuCycles = 733333;
			emuTotalCycles += avemu.Emulate(false, emuCycles);
			emuLastEmuTick = elapsetm;
		}
	}
	if (avemu.breakExecution) {
		if (avemu.breakReason == AdViEmulator::BREAK_EXCEPTION) {
			ui->statusbar->showMessage(avemu.breakException, 0);
		}
		emit emuStateChanged(4);
		SetState(false);
		return;
	}

	// Display speed
	if (elapsetm >= emuLastFPSTick) {
		int perc = emuTotalCycles * 100 / 733333;
		if (avemu.IsRunning()) setWindowTitle("AdViEmulator - " + QString::number(perc) + "%");
		emuLastFPSTick = elapsetm + 1000;
		emuTotalCycles = 0;
		// ---
		avemu.frameCnt = avemu.cpu.framecnt;
		avemu.cpu.framecnt = 0;
	}
}

void MainWindow::keyPressEvent(QKeyEvent *event)
{
	if (event->key() == Settings::o.keyButton1) {
		avemu.SetInput(AdViEmulator::BUTTON_1, true);
	} else if (event->key() == Settings::o.keyButton2) {
		avemu.SetInput(AdViEmulator::BUTTON_2, true);
	} else if (event->key() == Settings::o.keyButton3) {
		avemu.SetInput(AdViEmulator::BUTTON_3, true);
	} else if (event->key() == Settings::o.keyButton4) {
		avemu.SetInput(AdViEmulator::BUTTON_4, true);
	} else if (event->key() == Settings::o.keyStickDown) {
		avemu.SetInput(AdViEmulator::STICK_DOWN, true);
	} else if (event->key() == Settings::o.keyStickUp) {
		avemu.SetInput(AdViEmulator::STICK_UP, true);
	} else if (event->key() == Settings::o.keyStickRight) {
		avemu.SetInput(AdViEmulator::STICK_RIGHT, true);
	} else if (event->key() == Settings::o.keyStickLeft) {
		avemu.SetInput(AdViEmulator::STICK_LEFT, true);
	}
}

void MainWindow::keyReleaseEvent(QKeyEvent *event)
{
	if (event->key() == Settings::o.keyButton1) {
		avemu.SetInput(AdViEmulator::BUTTON_1, false);
	} else if (event->key() == Settings::o.keyButton2) {
		avemu.SetInput(AdViEmulator::BUTTON_2, false);
	} else if (event->key() == Settings::o.keyButton3) {
		avemu.SetInput(AdViEmulator::BUTTON_3, false);
	} else if (event->key() == Settings::o.keyButton4) {
		avemu.SetInput(AdViEmulator::BUTTON_4, false);
	} else if (event->key() == Settings::o.keyStickDown) {
		avemu.SetInput(AdViEmulator::STICK_DOWN, false);
	} else if (event->key() == Settings::o.keyStickUp) {
		avemu.SetInput(AdViEmulator::STICK_UP, false);
	} else if (event->key() == Settings::o.keyStickRight) {
		avemu.SetInput(AdViEmulator::STICK_RIGHT, false);
	} else if (event->key() == Settings::o.keyStickLeft) {
		avemu.SetInput(AdViEmulator::STICK_LEFT, false);
	}
}

void MainWindow::OnMenuFileOpen(void)
{
	TempState(true);
	QString fileName = QFileDialog::getOpenFileName(this, tr("Open ROM"), "", tr("Binary (*.bin);;Files (*.*)"));
	if (fileName != NULL) {
		if (OpenROM(fileName)) {
			addRecentList(fileName);
			avemu.Reset();
			emit emuStateChanged(4);
			if (Settings::o.autostart) SetState(true);
		}
	}
	TempState(false);
}

void MainWindow::OnMenuReloadROM(void)
{
	TempState(true);
	if (!openROM.isEmpty()) {
		if (OpenROM(openROM)) {
			avemu.Reset();
			emit emuStateChanged(4);
			if (Settings::o.autostart) SetState(true);
		}
	}
	TempState(false);
}

void MainWindow::addRecentList(QString filename)
{
	int i, j;
	for (i=0; i<8; i++) {
		if (Settings::o.recentFile[i].compare(filename) == 0) break;
	}
	if (i == 8) {
		// Don't exist, insert at last item
		for (j=7; j>0; j--) {
			Settings::o.recentFile[j] = Settings::o.recentFile[j-1];
		}
		Settings::o.recentFile[0] = filename;
	} else {
		// Does exist, insert at found item
		for (j=i; j>0; j--) {
			Settings::o.recentFile[j] = Settings::o.recentFile[j-1];
		}
		Settings::o.recentFile[0] = filename;
	}
	SaveSettings();
	rebuildRecentList();
}

void MainWindow::rebuildRecentList(void)
{
	QAction *ref;
	for (int i=0; i<8; i++) {
		switch (i) {
		case 0: ref = this->ui->actionRecent_ROM_1; break;
		case 1: ref = this->ui->actionRecent_ROM_2; break;
		case 2: ref = this->ui->actionRecent_ROM_3; break;
		case 3: ref = this->ui->actionRecent_ROM_4; break;
		case 4: ref = this->ui->actionRecent_ROM_5; break;
		case 5: ref = this->ui->actionRecent_ROM_6; break;
		case 6: ref = this->ui->actionRecent_ROM_7; break;
		case 7: ref = this->ui->actionRecent_ROM_8; break;
		}
		ref->setText(Settings::o.recentFile[i]);
		ref->setVisible(Settings::o.recentFile[i].length() > 0);
	}
}

void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
	event->acceptProposedAction();
}

void MainWindow::dragMoveEvent(QDragMoveEvent *event)
{
	event->acceptProposedAction();
}

void MainWindow::dragLeaveEvent(QDragLeaveEvent *event)
{
	event->accept();
}

void MainWindow::dropEvent(QDropEvent *event)
{
	const QMimeData* mimeData = event->mimeData();

	if (mimeData->hasUrls())
	{
		QString fileName = mimeData->urls().at(0).toLocalFile();
		if (OpenROM(fileName)) {
			addRecentList(fileName);
			avemu.Reset();
			emit emuStateChanged(4);
			if (Settings::o.autostart) SetState(true);
		}
		event->setDropAction(Qt::CopyAction);
		event->accept();
	}
}

void MainWindow::OnMenuRecentClear(void)
{
	for (int i=0; i<8; i++) Settings::o.recentFile[i].clear();
	SaveSettings();
	rebuildRecentList();
}

void MainWindow::OnMenuRecentRun(void)
{
	QAction *obj = (QAction *)sender();
	QVariant typvar = obj->property("Index");
	int idx = typvar.toInt();
	if (Settings::o.recentFile[idx].length() <= 0) return;
	QString fileName = Settings::o.recentFile[idx];
	if (OpenROM(fileName)) {
		addRecentList(fileName);
		avemu.Reset();
		emit emuStateChanged(4);
		if (Settings::o.autostart) SetState(true);
	}
}

void MainWindow::OnMenuReloadBIOS(void)
{
	// CPU (8048) firmware
	QFile avbiosf;
	avbiosf.setFileName(Settings::o.customcpu_enable ? Settings::o.customcpu_file : ":/firmware/avbios.bin");
	if (!avbiosf.open(QIODevice::ReadOnly)) {
		avbiosf.setFileName(":/firmware/avbios.bin");
		if (!avbiosf.open(QIODevice::ReadOnly)) {
			qCritical() << "Fail to open avbios.bin";
			return;
		}
	}
	avbiosf.read((char *)avemu.BIOS, 1024);
	avbiosf.close();

	// APU (COP411) firmware
	QFile avsoundf;
	avsoundf.setFileName(Settings::o.customapu_enable ? Settings::o.customapu_file : ":/firmware/avsound.bin");
	if (!avsoundf.open(QIODevice::ReadOnly)) {
		avsoundf.setFileName(":/firmware/avsound.bin");
		if (!avsoundf.open(QIODevice::ReadOnly)) {
			qCritical() << "Fail to open avsound.bin";
			return;
		}
	}
	avsoundf.read((char *)avemu.apu.rom, 512);
	avsoundf.close();
}

void MainWindow::OnMenuFileQuit(void)
{
	close();
}

void MainWindow::OnMenuStartEmu(void)
{
	SetState(true);
	emit emuStateChanged(0);
}

void MainWindow::OnMenuStopEmu(void)
{
	SetState(false);
	emit emuStateChanged(1);
}

void MainWindow::OnMenuCPUStepEmu(void)
{
	SetState(false);
	avemu.CPUSingleStep();
	emit emuStateChanged(2);
}

void MainWindow::OnMenuAPUStepEmu(void)
{
	SetState(false);
	avemu.APUSingleStep();
	emit emuStateChanged(2);
}

void MainWindow::OnMenuResetEmu(void)
{
	avemu.Reset();
	emit emuStateChanged(3);
}

void MainWindow::OnMenuOptions(void)
{
	TempState(true);
	SaveSettings();
	OptionsWindow optionswnd(this);
	optionswnd.resize(optionswnd.sizeHint());
	optionswnd.exec();
	LoadSettings();
	TempState(false);
}

void MainWindow::OnMenuEnableBreaks(bool enable)
{
	avemu.enableBreaks = enable;
}

void MainWindow::OnMenuClearAllBreaks(void)
{
	avemu.ClearAllBreaks();
}

void MainWindow::OnMenuCPUView(void)
{
	CPUWindow *viewwnd = new CPUWindow(this, &avemu);
	viewwnd->show();
}

void MainWindow::OnMenuAPUView(void)
{
	APUWindow *viewwnd = new APUWindow(this, &avemu);
	viewwnd->show();
}

void MainWindow::OnMenuMemoryView(void)
{
	MemoryWindow *viewwnd = new MemoryWindow(this, &avemu);
	viewwnd->show();
}

void MainWindow::OnMenuHardwareView(void)
{
	HardwareWindow *viewwnd = new HardwareWindow(this, &avemu);
	viewwnd->show();
}

void MainWindow::OnMenuTraceView(void)
{
	TraceWindow *viewwnd = new TraceWindow(this, &avemu);
	viewwnd->show();
}

void MainWindow::OnMenuResizeDefault(void)
{
	Qt::WindowStates currstat = windowState();
	setWindowState(windowState() & ~(Qt::WindowMinimized | Qt::WindowMaximized));
	this->resize(DEF_WIDTH, DEF_HEIGHT + this->statusBar()->height() + this->menuBar()->height());
	setWindowState(currstat);
}

void MainWindow::OnMenuResizeDefaultDouble(void)
{
	Qt::WindowStates currstat = windowState();
	setWindowState(windowState() & ~(Qt::WindowMinimized | Qt::WindowMaximized));
	this->resize(DEF_WIDTH*2, DEF_HEIGHT*2 + this->statusBar()->height() + this->menuBar()->height());
	setWindowState(currstat);
}

void MainWindow::OnMenuResizeDefaultHalf(void)
{
	Qt::WindowStates currstat = windowState();
	setWindowState(windowState() & ~(Qt::WindowMinimized | Qt::WindowMaximized));
	this->resize(DEF_WIDTH/2, DEF_HEIGHT/2 + this->statusBar()->height() + this->menuBar()->height());
	setWindowState(currstat);
}

void MainWindow::OnMenuCloseAllView(void)
{
	BaseViewWindow::CloseAll();
}

void MainWindow::OnMenuHelpAbout(void)
{
	TempState(true);
	AboutWindow aboutwnd(this);
	aboutwnd.resize(aboutwnd.sizeHint());
	aboutwnd.exec();
	TempState(false);
}

void MainWindow::goToCPUAddress(int addr)
{
	CPUWindow *viewwnd = new CPUWindow(this, &avemu);
	viewwnd->setCPUAddress(addr);
	viewwnd->show();
}

void MainWindow::goToAPUAddress(int addr)
{
	APUWindow *viewwnd = new APUWindow(this, &avemu);
	viewwnd->setAPUAddress(addr);
	viewwnd->show();
}
